﻿using System;
using System.Collections.Generic;
using System.Net.WebSockets;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

using Microsoft.AspNetCore.Http;
using System.Net.Http;
using System.Reflection;
using Microsoft.AspNetCore.Mvc;
using System.Text.RegularExpressions;
using System.Linq;

namespace MicroApi.Core.WebSockets
{
    public class WebSocketConnection : IAsyncDisposable
    {


        private readonly WebSocket webSocket;

        public Guid Guid { get; private set; }

        private readonly HashSet<string> groups = new HashSet<string>();

        public HttpContext HttpContext { get; private set; }

        public WebSocketConnectionManager SocketConnectionManager { get; set; }

        private readonly object socketService;

        private readonly MethodInfo onOpenMethod;

        private readonly MethodInfo onCloseMethod;

        private readonly MethodInfo onMessageMethod;


        private readonly MethodInfo onErrorMethod;

        private readonly object[] openArgs;

        private readonly IClientSession clientSession;
        internal WebSocketConnection(HttpContext httpContex, WebSocketConnectionManager socketConnectionManager, WebSocket webSocket, object socketService, params object[] openArgs)
        {
            this.SocketConnectionManager = socketConnectionManager;
            this.webSocket = webSocket ?? throw new NullReferenceException("webSocket");
            this.HttpContext = httpContex;
            this.openArgs = openArgs ?? new object[0];
            this.socketService = socketService;
            Guid = Guid.NewGuid();
            var type = socketService.GetType();
            onOpenMethod = type.GetMethod("OnOpen", BindingFlags.Public | BindingFlags.Instance);
            onCloseMethod = type.GetMethod("OnClose", BindingFlags.Public | BindingFlags.Instance);
            onErrorMethod = type.GetMethod("OnError", BindingFlags.Public | BindingFlags.Instance);
            onMessageMethod = type.GetMethod("OnMessage", BindingFlags.Public | BindingFlags.Instance);
            clientSession = new ClientSession(new WebSocketConnection[] { this }, WebSocketConnectionManager.jsonSerializerSettings);
        }

        public bool Is(string groupName)
        {
            if (groupName.Contains(","))
            {
                var groupNames = groupName.Split(",");
                return groups.Contains(groupNames.Contains);
            }
            return groups.Contains(groupName);
        }

        public WebSocketConnection AddGroup(string groupName)
        {
            groups.Add(groupName);
            return this;
        }

        public override bool Equals(object obj)
        {
            if (this == obj) return true;
            if (obj == null || !(obj is WebSocketConnection web)) return false;
            return Guid.Equals(web.Guid);
        }

        public override int GetHashCode()
        {
            return Guid.GetHashCode();
        }
        public WebSocketConnection RemoveGroup(string groupName)
        {
            groups.Remove(groupName);
            return this;
        }
        public IClientSession ClientSession => clientSession;

        internal async Task ReceiveAsync()
        {
            try
            {
                await Call(onOpenMethod);
            }
            catch (Exception e)
            {
                OnError(e.InnerException ?? e);
                await DisposeAsync();
                return;
            }
         

            var buffer = new ArraySegment<byte>(new byte[1024]);
            while (webSocket.State == WebSocketState.Open)
            {
                try
                {
                    WebSocketReceiveResult receiveResult;
                    var array = new byte[0];
                    do
                    {
                        receiveResult = await webSocket.ReceiveAsync(buffer, HttpContext.RequestAborted);
                        Array.Resize(ref array, array.Length + receiveResult.Count);
                        Array.Copy(buffer.Array, 0, array, array.Length - receiveResult.Count, receiveResult.Count);

                    } while (!receiveResult.EndOfMessage);

                    if (receiveResult.MessageType != WebSocketMessageType.Close)
                    {
                        _ = ReceiveAsync(array, receiveResult.MessageType)
                             .ContinueWith(t =>
                             {
                                 if (t.IsFaulted)
                                 {
                                     OnError(t.Exception);
                                 }
                             });
                    }


                }
                catch (OperationCanceledException e)
                {
                    break;
                }
                catch (WebSocketException e)
                {
                    if (e.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely)
                    {
                        break;
                    }
                    OnError(e);
                }
                catch (Exception e)
                {
                    OnError(e);
                    break;
                }
            }
            await DisposeAsync();
        }



        internal async Task SendAsync(string str)
        {
            await webSocket.SendAsync(Encoding.UTF8.GetBytes(str), WebSocketMessageType.Text, true, HttpContext.RequestAborted);
        }

        internal async Task SendAsync(byte[] buffer)
        {
            await webSocket.SendAsync(buffer, WebSocketMessageType.Binary, true, HttpContext.RequestAborted);
        }

        private async Task ReceiveAsync(byte[] buffer, WebSocketMessageType messageType)
        {
            if (messageType == WebSocketMessageType.Binary)
            {
                await Call(onMessageMethod, buffer);
            }
            else if (messageType == WebSocketMessageType.Text)
            {
                await Call(onMessageMethod, Encoding.UTF8.GetString(buffer));
            }
        }

        private async Task Call(MethodInfo methodInfo, params object[] args2)
        {
            if (methodInfo != null)
            {
                var parameters = methodInfo.GetParameters();
                var args = new List<object>();
                var newargs = args2.Concat(openArgs);
                foreach (var parameter in parameters)
                {
                    if (parameter.ParameterType.Equals(typeof(WebSocketConnection)))
                    {
                        args.Add(this);
                    }
                    else
                    {
                        args.Add(newargs.Where(a => a != null).Where(a => parameter.ParameterType.IsAssignableFrom(a.GetType()))
                            .FirstOrDefault());
                    }
                }
                var res = methodInfo.Invoke(socketService, args.ToArray());
                if (res is Task task)
                {
                    await task;
                }
            }
        }



        private void OnError(Exception exception)
        {
            if (onErrorMethod == null)
            {
                Console.WriteLine(exception.ToString());
            }
            else
            {
                Call(onErrorMethod, exception).GetAwaiter();
            }
        }
        public async ValueTask DisposeAsync()
        {
            SocketConnectionManager.Remove(Guid);
            try
            {
                await Call(onCloseMethod);
            }
            catch (Exception e)
            {
                await Call(onErrorMethod, e);
            }
            try
            {
                if (webSocket.State == WebSocketState.Open || webSocket.State == WebSocketState.CloseSent)
                {
                    await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing the connection.", CancellationToken.None);
                }
                webSocket.Dispose();
            }
            catch(Exception e)
            {
                Console.WriteLine(e.ToString());
            }
    
        }
    }
}
